    // Copyright (C) 2011  Davis E. King (davis@dlib.net)
    // License: Boost Software License   See LICENSE.txt for the full license.
    #ifndef DLIB_SQLiTE_H_
    #define DLIB_SQLiTE_H_

    #include "sqlite_abstract.h"

    #include <iostream>
    #include <limits>
    #include <memory>
    #include <vector>

    #include "../algs.h"
    #include <sqlite3.h>
    #include "../serialize.h"

    // --------------------------------------------------------------------------------------------

    namespace dlib
    {

    // --------------------------------------------------------------------------------------------

        struct sqlite_error : public error
        {
            sqlite_error(const std::string& message): error(message) {}
        };

    // --------------------------------------------------------------------------------------------

        namespace impl
        {
            struct db_deleter
            {
                void operator()(
                    sqlite3* db
                )const
                {
                    sqlite3_close(db);
                }
            };
        }

    // --------------------------------------------------------------------------------------------

        class database : noncopyable
        {
        public:
            database(
            )
            {
            }

            database (
                const std::string& file
            )
            {
                open(file);
            }

            bool is_open (
            ) const
            {
                return db.get() != 0;
            }

            void open (
                const std::string& file
            )
            {
                filename = file;
                sqlite3* ptr = 0;
                int status = sqlite3_open(file.c_str(), &ptr);
                db.reset(ptr, impl::db_deleter());
                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

            const std::string& get_database_filename (
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(is_open() == true,
                    "\t std::string database::get_database_filename()"
                    << "\n\t The database must be opened before calling this routine."
                    << "\n\t this: " << this
                    );

                return filename;
            }

            inline void exec (
                const std::string& sql_statement
            );

            int64 last_insert_rowid (
            ) const
            {
                return sqlite3_last_insert_rowid(db.get());
            }

        private:

            friend class statement;

            std::string filename;
            std::shared_ptr<sqlite3> db;
        };

    // --------------------------------------------------------------------------------------------

        class statement : noncopyable
        {
        public:
            statement (
                database& db_,
                const std::string sql_statement
            ) :
                needs_reset(false),
                step_status(SQLITE_DONE),
                at_first_step(true),
                db(db_.db),
                stmt(0),
                sql_string(sql_statement)
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(db_.is_open() == true,
                            "\t statement::statement()"
                            << "\n\t The database must be opened before calling this routine."
                            << "\n\t this: " << this
                );

                int status = sqlite3_prepare_v2(db.get(),
                                             sql_string.c_str(),
                                             sql_string.size()+1,
                                             &stmt,
                                             NULL);

                if (status != SQLITE_OK)
                {
                    sqlite3_finalize(stmt);
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
                if (stmt == 0)
                {
                    throw sqlite_error("Invalid SQL statement");
                }
            }

            ~statement(
            )
            {
                sqlite3_finalize(stmt);
            }

            void exec(
            )
            {
                reset();

                step_status = sqlite3_step(stmt);
                needs_reset = true;
                if (step_status != SQLITE_DONE && step_status != SQLITE_ROW)
                {
                    if (step_status == SQLITE_ERROR)
                        throw sqlite_error(sqlite3_errmsg(db.get()));
                    else if (step_status == SQLITE_BUSY)
                        throw sqlite_error("statement::exec() failed.  SQLITE_BUSY returned");
                    else
                        throw sqlite_error("statement::exec() failed.");
                }
            }

            bool move_next (
            )
            {
                if (step_status == SQLITE_ROW)
                {
                    if (at_first_step)
                    {
                        at_first_step = false;
                        return true;
                    }
                    else
                    {
                        step_status = sqlite3_step(stmt);
                        if (step_status == SQLITE_DONE)
                        {
                            return false;
                        }
                        else if (step_status == SQLITE_ROW)
                        {
                            return true;
                        }
                        else
                        {
                            throw sqlite_error(sqlite3_errmsg(db.get()));
                        }
                    }
                }
                else
                {
                    return false;
                }
            }

            unsigned long get_num_columns(
            ) const
            {
                if( (at_first_step==false) && (step_status==SQLITE_ROW))
                {
                    return sqlite3_column_count(stmt);
                }
                else
                {
                    return 0;
                }
            }

            const std::string& get_sql_string (
            ) const
            {
                return sql_string;
            }

            template <typename T>
            typename enable_if_c<std::numeric_limits<T>::is_integer>::type get_column (
                unsigned long idx,
                T& item
            ) const
            {
                // unsigned ints won't fit into int all the time so put those into 64bit ints.
                if (sizeof(T) < sizeof(int) || (sizeof(T)==sizeof(int) && is_signed_type<T>::value))
                    item = get_column_as_int(idx);
                else
                    item = get_column_as_int64(idx);
            }

            void get_column(unsigned long idx, std::string& item) const { item = get_column_as_text(idx); }
            void get_column(unsigned long idx, float& item      ) const { item = get_column_as_double(idx); }
            void get_column(unsigned long idx, double& item     ) const { item = get_column_as_double(idx); }
            void get_column(unsigned long idx, long double& item) const { item = get_column_as_double(idx); }

            template <typename T>
            typename disable_if_c<std::numeric_limits<T>::is_integer>::type get_column (
                unsigned long idx,
                T& item
            ) const
            {
                get_column_as_object(idx, item);
            }

            const std::vector<char> get_column_as_blob (
                unsigned long idx
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(idx < get_num_columns(),
                            "\t std::vector<char> statement::get_column_as_blob()"
                            << "\n\t Invalid column index."
                            << "\n\t idx:  " << idx
                            << "\n\t this: " << this
                );

                const char* data = static_cast<const char*>(sqlite3_column_blob(stmt, idx));
                const int size = sqlite3_column_bytes(stmt, idx);

                return std::vector<char>(data, data+size);
            }

            template <typename T>
            void get_column_as_object (
                unsigned long idx,
                T& item
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(idx < get_num_columns(),
                            "\t void statement::get_column_as_object()"
                            << "\n\t Invalid column index."
                            << "\n\t idx:  " << idx
                            << "\n\t this: " << this
                );

                const char* data = static_cast<const char*>(sqlite3_column_blob(stmt, idx));
                const int size = sqlite3_column_bytes(stmt, idx);
                std::istringstream sin(std::string(data,size));
                deserialize(item, sin);
            }

            const std::string get_column_as_text (
                unsigned long idx
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(idx < get_num_columns(),
                            "\t std::string statement::get_column_as_text()"
                            << "\n\t Invalid column index."
                            << "\n\t idx:  " << idx
                            << "\n\t this: " << this
                );

                const char* data = reinterpret_cast<const char*>(sqlite3_column_text(stmt, idx));
                if (data != 0)
                    return std::string(data);
                else
                    return std::string();
            }

            double get_column_as_double (
                unsigned long idx
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(idx < get_num_columns(),
                            "\t double statement::get_column_as_double()"
                            << "\n\t Invalid column index."
                            << "\n\t idx:  " << idx
                            << "\n\t this: " << this
                );

                return sqlite3_column_double(stmt, idx);
            }

            int get_column_as_int (
                unsigned long idx
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(idx < get_num_columns(),
                            "\t int statement::get_column_as_int()"
                            << "\n\t Invalid column index."
                            << "\n\t idx:  " << idx
                            << "\n\t this: " << this
                );

                return sqlite3_column_int(stmt, idx);
            }

            int64 get_column_as_int64 (
                unsigned long idx
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(idx < get_num_columns(),
                            "\t int64 statement::get_column_as_int64()"
                            << "\n\t Invalid column index."
                            << "\n\t idx:  " << idx
                            << "\n\t this: " << this
                );

                return sqlite3_column_int64(stmt, idx);
            }

            const std::string get_column_name (
                unsigned long idx
            ) const
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(idx < get_num_columns(),
                            "\t std::string statement::get_column_name()"
                            << "\n\t Invalid column index."
                            << "\n\t idx:  " << idx
                            << "\n\t this: " << this
                );

                return std::string(sqlite3_column_name(stmt,idx));
            }

            unsigned long get_max_parameter_id (
            ) const
            {
                return sqlite3_limit(db.get(), SQLITE_LIMIT_VARIABLE_NUMBER, -1);
            }

            unsigned long get_parameter_id (
                const std::string& name
            ) const
            {
                return sqlite3_bind_parameter_index(stmt, name.c_str());
            }

            template <typename T>
            typename enable_if_c<std::numeric_limits<T>::is_integer>::type bind (
                unsigned long idx,
                const T& item
            )
            {
                // unsigned ints won't fit into int all the time so put those into 64bit ints.
                if (sizeof(T) < sizeof(int) || (sizeof(T)==sizeof(int) && is_signed_type<T>::value))
                    bind_int(idx, item);
                else
                    bind_int64(idx, item);
            }

            void bind(unsigned long idx, const std::string& item) { bind_text(idx, item); }
            void bind(unsigned long idx, const float& item      ) { bind_double(idx, item); }
            void bind(unsigned long idx, const double& item     ) { bind_double(idx, item); }
            void bind(unsigned long idx, const long double& item) { bind_double(idx, item); }

            template <typename T>
            typename disable_if_c<std::numeric_limits<T>::is_integer>::type bind (
                unsigned long idx,
                const T& item
            )
            {
                bind_object(idx, item);
            }

            void bind_blob (
                unsigned long parameter_id,
                const std::vector<char>& item
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(1 <= parameter_id && parameter_id <= get_max_parameter_id(),
                            "\t void statement::bind_blob()"
                            << "\n\t Invalid parameter id."
                            << "\n\t parameter_id:           " << parameter_id
                            << "\n\t get_max_parameter_id(): " << get_max_parameter_id()
                            << "\n\t this:                   " << this
                );

                reset();
                int status = sqlite3_bind_blob(stmt, parameter_id, &item[0], item.size(), SQLITE_TRANSIENT);

                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

            template <typename T>
            void bind_object (
                unsigned long parameter_id,
                const T& item
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(1 <= parameter_id && parameter_id <= get_max_parameter_id(),
                            "\t void statement::bind_object()"
                            << "\n\t Invalid parameter id."
                            << "\n\t parameter_id:           " << parameter_id
                            << "\n\t get_max_parameter_id(): " << get_max_parameter_id()
                            << "\n\t this:                   " << this
                );

                reset();
                std::ostringstream sout;
                serialize(item, sout);
                const std::string& str = sout.str();
                int status = sqlite3_bind_blob(stmt, parameter_id, str.data(), str.size(), SQLITE_TRANSIENT);

                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

            void bind_double (
                unsigned long parameter_id,
                const double& item
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(1 <= parameter_id && parameter_id <= get_max_parameter_id(),
                            "\t void statement::bind_double()"
                            << "\n\t Invalid parameter id."
                            << "\n\t parameter_id:           " << parameter_id
                            << "\n\t get_max_parameter_id(): " << get_max_parameter_id()
                            << "\n\t this:                   " << this
                );

                reset();
                int status = sqlite3_bind_double(stmt, parameter_id, item);

                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

            void bind_int (
                unsigned long parameter_id,
                const int& item
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(1 <= parameter_id && parameter_id <= get_max_parameter_id(),
                            "\t void statement::bind_int()"
                            << "\n\t Invalid parameter id."
                            << "\n\t parameter_id:           " << parameter_id
                            << "\n\t get_max_parameter_id(): " << get_max_parameter_id()
                            << "\n\t this:                   " << this
                );

                reset();
                int status = sqlite3_bind_int(stmt, parameter_id, item);

                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

            void bind_int64 (
                unsigned long parameter_id,
                const int64& item
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(1 <= parameter_id && parameter_id <= get_max_parameter_id(),
                            "\t void statement::bind_int64()"
                            << "\n\t Invalid parameter id."
                            << "\n\t parameter_id:           " << parameter_id
                            << "\n\t get_max_parameter_id(): " << get_max_parameter_id()
                            << "\n\t this:                   " << this
                );

                reset();
                int status = sqlite3_bind_int64(stmt, parameter_id, item);

                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

            void bind_null (
                unsigned long parameter_id
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(1 <= parameter_id && parameter_id <= get_max_parameter_id(),
                            "\t void statement::bind_null()"
                            << "\n\t Invalid parameter id."
                            << "\n\t parameter_id:           " << parameter_id
                            << "\n\t get_max_parameter_id(): " << get_max_parameter_id()
                            << "\n\t this:                   " << this
                );

                reset();
                int status = sqlite3_bind_null(stmt, parameter_id);

                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

            void bind_text (
                unsigned long parameter_id,
                const std::string& item
            )
            {
                // make sure requires clause is not broken
                DLIB_ASSERT(1 <= parameter_id && parameter_id <= get_max_parameter_id(),
                            "\t void statement::bind_text()"
                            << "\n\t Invalid parameter id."
                            << "\n\t parameter_id:           " << parameter_id
                            << "\n\t get_max_parameter_id(): " << get_max_parameter_id()
                            << "\n\t this:                   " << this
                );

                reset();
                int status = sqlite3_bind_text(stmt, parameter_id, item.c_str(), -1, SQLITE_TRANSIENT);

                if (status != SQLITE_OK)
                {
                    throw sqlite_error(sqlite3_errmsg(db.get()));
                }
            }

        private:

            void reset()
            {
                if (needs_reset)
                {
                    if (sqlite3_reset(stmt) != SQLITE_OK)
                    {
                        step_status = SQLITE_DONE;
                        throw sqlite_error(sqlite3_errmsg(db.get()));
                    }
                    needs_reset = false;
                    step_status = SQLITE_DONE;
                    at_first_step = true;
                }
            }

            bool needs_reset; // true if sqlite3_step() has been called more recently than sqlite3_reset()
            int step_status;
            bool at_first_step;

            std::shared_ptr<sqlite3> db;
            sqlite3_stmt* stmt;
            std::string sql_string;
        };

    // --------------------------------------------------------------------------------------------

        void database::
        exec (
            const std::string& sql_statement
        )
        {
            // make sure requires clause is not broken
            DLIB_ASSERT(is_open() == true,
                        "\t void database::exec()"
                        << "\n\t The database must be opened before calling this routine."
                        << "\n\t this: " << this
            );

            statement(*this, sql_statement).exec();
        }

    // --------------------------------------------------------------------------------------------

    }

    #endif // DLIB_SQLiTE_H_